HPy · Unit 2

2.1–2.2 Loops

for loops and range · nested loops · while loops · break and continue

2.1 Getting Started

Running the same code, again and again

A loop runs the same block of code repeatedly. Python gives you two kinds: for loops and while loops. Loops pair especially well with strings, since a string can be any length, and a loop doesn't care how many times it has to run. Together, loops and strings let you solve far more interesting problems than one-shot, straight-line code ever could.

Before you run anything in this unit, predict it first. Every trace in this deck asks you to guess the next line's effect before you reveal it. That habit is what actually builds tracing skill.
FRQ · Warm-Up

Known count, or unknown count?

Think of something you repeat in real life. Is it something you repeat a known number of times (like doing 20 push-ups), or something you repeat until a condition is met (like shuffling cards until they feel mixed)? Describe one of each.

2.2.1 for Loops and range

Looping a known number of times

A for loop is the tool for when you already know how many passes you need, or you know exactly what you're looping over. Python's range is the most common thing to loop over.

The General Form

A name for each value

  • The name right after for, often something like x, is the loop variable (also called the looping variable).
  • range() is a function which generates a sequence of numbers from 0 up to (but not including) the specified number.
  • In other words, range(4) is exclusive. It produces the sequence 0, 1, 2, 3, up to but not including 4.
  • The indented body runs once per value. Each trip through the body is a pass, or an iteration.
  • On every pass, Python assigns the loop variable the next value from the range, then runs the body.
main.py
for LOOPVAR in range(N):
    BODY
FRQ · Predict First

What does this print, in order?

main.py
for x in range(4):
    print(x)
Try This

Watch x update, one pass at a time

Line 1 assigns the next value to x, then Python enters the body. x's new value only becomes visible once the caret has moved past line 1, and print(x)'s output only becomes visible once the caret moves past line 2, back up to check the range again.

main.py
1for x in range(4):
2    print(x)
x
pass
console
0 1 2 3
A Real Example

A running total

total is a running total (also called a tally): a variable that carries a value across passes, updated a little more on every one. Watch it grow.

main.py
1def sumToN(n):
2    total = 0
3    for x in range(n+1):
4        total += x
5    return total
6 
7print(sumToN(4))
total
console
10

n is 4, so range(n+1) is range(5): five passes, x = 0, 1, 2, 3, 4.

FRQ · Reason It Out

Why range(n+1), not range(n)?

sumToN(n) uses range(n+1) instead of range(n). What is it trying to include that range(n) alone would leave out?

MCQ

Swap in range(n) instead

main.py
def sumToN(n):
    total = 0
    for x in range(n):
        total += x
    return total

With this change, what does sumToN(4) return?

  • A 10, exactly the same
  • B 6, since it would miss the 4
  • C It would crash
  • D 0
Using range(start, end)

Choosing where to start

With two arguments, range treats the first as the start value (inclusive) and the second as the end value (still exclusive).

main.py
range(1, 5)  # 1, 2, 3, 4
See For Yourself · Predict First

This factorial is broken

factorial(n) should multiply the integers from 1 to n. This version copies sumToN and swaps += for *=. Predict the output, then run it.

main.py
def factorial(n):
    total = 0
    for x in range(n+1):
        total *= x
    return total

print(factorial(4))
What Went Wrong

Anything times zero is zero

range(n+1) starting at the default 0 includes 0 itself. That was fine for sumToN, adding 0 changes nothing, but for a product, multiplying by 0 even once wipes out the whole running total. The fix: start the range at 1 instead of 0, and start total at 1 too, since it's the identity value for multiplication.

The accumulator's starting value depends on the operation. A running sum starts at 0 (adding 0 is a no-op). A running product starts at 1 (multiplying by 1 is a no-op).
The Fix

Start at 1, not 0

total now starts at 1, and range(1, n+1) skips the 0 entirely.

main.py
1def factorial(n):
2    total = 1
3    for x in range(1, n+1):
4        total *= x
5    return total
6 
7print(factorial(4))
total
console
24

range(1, 5): four passes, x = 1, 2, 3, 4.

Checkpoint 1

Which block produces this output?

output
5
6
7
8
  • A for i in range(2, 9): print(i)
  • B for i in range(5, 8): print(i)
  • C for i in range(5, 9): print(i)
  • D for i in range(2, 8): print(i)
Using range(start, end, step)

Skipping by more than 1

A third argument is the step: the amount added to the loop variable on every pass. Without it, the step defaults to 1.

main.py
range(20, 40, 5)  # 20, 25, 30, 35
Try This

The step is the jump size

Each pass, x jumps by 5 instead of the default 1.

main.py
1for x in range(20, 40, 5):
2    print(x)
x
console
20 25 30 35
Looping Backwards

A step of -1

To loop backwards over 12, 11, 10, a first attempt, range(12, 10, -1), almost works: it only prints 12 and 11, because the end is still exclusive. The fix drops the end to 9, one past the last value we want.

In general, to loop backwards from y down to x inclusively: range(y, x-1, -1).
main.py
1for x in range(12, 9, -1):
2    print(x)
x
console
12 11 10
Checkpoint 2

Which block produces this output?

output
6
9
12
  • A for i in range(5, 13, 3): print(i)
  • B for i in range(6, 13, 3): print(i)
  • C for i in range(3, 6, 15): print(i)
  • D for i in range(6, 12, 3): print(i)
Checkpoint 3

Which block produces this output?

output
15
14
13
12
  • A for i in range(15, 12, -1): print(i)
  • B for i in range(16, 11, -1): print(i)
  • C for i in range(15, 11, -1): print(i)
  • D for i in range(16, 12, -1): print(i)
Recap

for loops and range

  • for LOOPVAR in range(N): assigns the loop variable a new value each pass, then runs the body. range(N) is exclusive of N.
  • range(start, end) and range(start, end, step) control where the loop begins, ends, and how far it jumps each pass.
  • A running total (or product) needs a starting value that's a no-op for its operation: 0 for addition, 1 for multiplication.
  • To loop backwards from y down to x inclusively, use range(y, x-1, -1).
2.2.2 Nested for Loops

A loop inside a loop

Just like an if can nest inside another if, a for loop can nest inside another for loop. The two loop variables update at very different rates, and seeing exactly how is the whole point of this section.

Outer and Inner

The inner loop runs to completion

  • The outer loop controls the slower-changing variable. Each of its passes is one trip through the whole inner loop.
  • The inner loop runs completely, start to finish, for every single pass of the outer loop.
  • So if the outer loop has 3 passes and the inner loop has 3 passes each, the inner body runs 3 × 3 = 9 times total.
main.py
for x in range(3):
    for y in range(3):
        BODY
FRQ · Predict First

How many lines total?

Predict the full output, in order. How many lines total, and what are the first three rows?

main.py
print('x', 'y')
for x in range(3):
    for y in range(3):
        print(x, y)
The Full Trace

x barely moves, y moves constantly

Watch how often each box updates. y gets reassigned on nearly every pass. x only changes once the entire inner loop has finished, and its old value keeps showing until then, since Python hasn't reassigned it yet.

main.py
1print('x', 'y')
2for x in range(3):
3    for y in range(3):
4        print(x, y)
x
y
console
x  y 0  0 0  1 0  2 1  0 1  1 1  2 2  0 2  1 2  2
What You Just Watched

Two loop variables, two speeds

y updates on almost every pass through the console: once per inner-loop iteration.
x updates only 3 times total: once each time the entire inner loop finishes.

This is why nested loops are so useful for grids, tables, and anything with rows and columns: the outer variable tracks the row, the inner variable sweeps across every column before the row advances.

Checkpoint 1

Which nesting produces this output?

output
2 30
2 32
2 34
8 30
8 32
8 34
  • A for j in range(2,9,6): for i in range(30,36,2): print(i, j)
  • B for i in range(30,36,2): for j in range(2,9,6): print(i, j)
  • C for i in range(2,9,6): for j in range(30,36,2): print(i, j)
  • D for j in range(30,36,2): for i in range(2,9,6): print(i, j)
Dependent Inner Loops

The inner range can depend on x

The inner loop's range doesn't have to be fixed. It can use the outer loop variable, so each outer pass gets a differently sized inner loop.

main.py
for x in range(4):
    for y in range(x+1):
        print(x, y)
Try This

y stops earlier each time

range(x+1) is re-evaluated fresh at the start of every inner loop, using whatever x is at that moment. When x is 0, the inner loop only has one pass.

main.py
1print('x', 'y')
2for x in range(4):
3    for y in range(x+1):
4        print(x, y)
x
y
console
x  y 0  0 1  0 1  1 2  0 2  1 2  2 3  0 3  1 3  2 3  3
Checkpoint 2

Why does x = 0 contribute no rows?

With y looping over range(x) instead of range(x+1), the very first outer pass (x is 0) contributes zero rows to the output. What's the real reason?

  • A Python skips the inner loop entirely whenever the outer variable is 0.
  • B range(0) is a valid range with zero values in it, so the inner loop simply runs zero passes.
  • C The print statement is unreachable when x is 0.
  • D range(0) raises an error that Python silently ignores.
Recap

Nested for loops

  • The inner loop runs to completion for every single pass of the outer loop, so the inner variable updates far more often.
  • The inner loop's range can depend on the outer loop variable, letting each outer pass produce a differently sized inner loop.
  • An empty range, like range(0), simply produces zero passes. Nothing crashes, nothing runs.
2.2.3 while Loops

Looping until something changes

Sometimes you don't know how many passes you'll need before you start. A while loop keeps running so long as a condition is True, re-checking it before every pass, closer to how an if statement behaves than a for loop does.

for vs while

Known count, or unknown?

  • Use for when you already know how many passes you need, or exactly what you're looping over.
  • Use while when the number of passes depends on something you can't know in advance, like user input.
  • A while loop re-checks its test before every single pass, and stops the moment the test is False.
main.py
while TEST:
    BODY
Try This

Add numbers until total exceeds 100

We can't know how many passes this needs until we see what the user types. For this trace, imagine the user enters 30, then 40, then 50.

main.py
1total = 0
2while total <= 100:
3    print('Total so far:', total)
4    nextNumber = int(input('Enter next number: '))
5    total += nextNumber
6print('Done, final total:', total)
total
nextNumber
console
Total so far: 0 Enter next number: 30 Total so far: 30 Enter next number: 40 Total so far: 70 Enter next number: 50 Done, final total: 120

120 > 100, so the loop stops before a fourth pass.

Checkpoint

Guessing a secret number

A program needs to keep asking the user to guess a secret number until they finally guess correctly. Which loop is the right tool, and why?

  • A A for loop, since you always know range(10) will work for guessing games.
  • B A while loop, since the number of guesses needed can't be known before the program runs.
  • C A for loop, since guessing is a kind of counting.
  • D It doesn't matter, both loop types handle this equally well.
FRQ · Predict First

What if the user types a float?

nextNumber = int(input(...)) converts the typed text to an int. What happens if the user types a float, like 3.5, instead of a whole number?

See For Yourself

Try it with your own numbers

Run it and enter whatever numbers you like. Try entering a float. Try entering a negative number. Try entering text that isn't a number at all, and see what happens.

Recap

while loops

  • A while TEST: loop keeps running as long as TEST is True, re-checking before every pass.
  • Use it when you don't know the number of passes in advance, most often because the loop depends on user input.
  • Forgetting to update whatever the test depends on is the classic while-loop bug: it produces an infinite loop.
2.2.4 break and continue

Changing course mid-loop

break and continue both alter a loop's normal flow from inside the body. Used well, they can be very handy. Used too often, they make code harder to follow, so use them sparingly.

break Statements

Exit the loop, immediately

  • A break inside a loop body exits the entire loop right away, skipping any remaining lines in that pass and every future pass.
  • It's especially useful inside a while loop, when you can only tell it's time to stop after you're already inside the loop body.
  • The idiom while True: means "loop forever", an infinite loop, unless something inside the body eventually breaks out of it.
main.py
while True:
    BODY
    if TEST:
        break
Try This

Stop when the user enters 0

Imagine the user enters 30, then 40, then 0. Watch what happens to line 7, total += nextNumber, on the last pass.

main.py
1total = 0
2while True:
3    print('Total so far:', total)
4    nextNumber = int(input('Enter next number: '))
5    if nextNumber == 0:
6        break
7    total += nextNumber
8print('Done, final total:', total)
total
nextNumber
console
Total so far: 0 Enter next number: 30 Total so far: 30 Enter next number: 40 Total so far: 70 Enter next number: 0 Done, final total: 70

On the last pass, line 7 never runs. break jumps straight to line 8.

An Alternative

The same logic, no break

You can write this without break by looping on nextNumber != 0 directly. That means nextNumber needs a starting value that isn't 0 before the loop even begins, and None is a natural choice.

Both versions are common. Understand both.

main.py
total = 0
nextNumber = None
while nextNumber != 0:
    nextNumber = int(input('Enter next number: '))
    if nextNumber != 0:
        total += nextNumber
Checkpoint

Inputs: 5, 10, 15, then 20

main.py
total = 0
while True:
    nextNumber = int(input('Enter next: '))
    if nextNumber == 20:
        break
    total += nextNumber
print(total)

If the user enters 5, 10, 15, then 20, what does this print?

  • A It doesn't print anything
  • B 0
  • C 30
  • D 50
continue Statements

Skip the rest of this pass only

continue is similar to break, but gentler: it exits only the current pass, not the whole loop. Python jumps straight back to the top of the loop and carries on with the next pass, as if the rest of that pass's body was skipped.

main.py
for i in range(12):
    if i % 2 == 0:
        continue
    print(i)
Try This

Even i gets skipped

When i is even, continue jumps straight back to line 1 and line 4 never runs for that pass. When i is odd, the if body is skipped instead, and line 4 runs normally.

main.py
1for i in range(12):
2    if i % 2 == 0:
3        continue
4    print(i)
i
console
1 3
…the pattern continues through i = 11.
Full output: 1  3  5  7  9  11
Checkpoint 2

What if we used break instead?

In the previous example, what happens if you use break instead of continue?

  • A The code runs and prints only even numbers.
  • B The code crashes.
  • C Something else.
  • D The code runs but prints nothing.
  • E The code runs unchanged, printing only odd numbers.
  • F The code runs and prints both even and odd numbers.
Use continue Sparingly

Two clearer rewrites

The continue version above shows how it works, but it's not the clearest way to write this. Only reach for continue when it genuinely makes code easier to read, which is rare.

no continue
for i in range(12):
    if i % 2 == 1:
        print(i)
step instead
for i in range(1, 12, 2):
    print(i)
Build It · Your Turn

Stop once the total passes 50

Write a program that adds up numbers the user enters, one at a time, stopping as soon as the running total goes over 50. Then print how many numbers it took. This could be written with a while loop, or with a for loop and a break. Pick whichever feels like the right tool, you'll need to defend your choice next.

Postboard · Exit Ticket

What did you choose, and why?

Did you reach for a while loop, or a for loop with a break? Why did that feel like the right tool for this task, and could the other approach have worked too?

Recap

break and continue

  • break exits the entire loop immediately, often paired with while True: to build a loop that stops from the inside.
  • continue skips only the rest of the current pass and jumps back to the top of the loop for the next one.
  • Both are optional. A well-chosen loop condition, or a step in range, often reads more clearly than either one.
Unit Recap

2.1–2.2, all together

  • for LOOPVAR in range(...): for a known number of passes
  • while TEST: for an unknown number, re-checked every pass.
  • Nested loops run the inner loop to completion on every single pass of the outer loop. The inner variable updates far more often than the outer one.
  • break exits a loop entirely. continue skips only the current pass. Both are optional, and both are easy to overuse.
Guided Exercise · As a Class

Write isPrime(n)

Write the function isPrime(n) that takes a possibly-negative integer n and returns True if n is prime, and False otherwise.

The starter code on the next slide includes a set of tests. A loop is the natural tool for checking whether any smaller number divides evenly into n.

Build It · isPrime

Make every assert pass

Guided Exercise · Your Turn

Write nthPrime(n)

Write the function nthPrime(n) that takes a non-negative integer n and returns the nth prime number. 2 is the 0th prime number, 3 is the 1st prime number, and so on.

Build It · nthPrime

Make every assert pass

Guided Exercise · As a Class

Write reverseNumber(n)

Write the function reverseNumber(n) that takes an integer n and returns an integer with its digits in the reverse order of the digits in n.

Build It · reverseNumber

Make every assert pass

Guided Exercise · As a Class

Write mostFrequentDigit(n)

Write the function mostFrequentDigit(n) that takes a possibly-negative integer n and returns the digit from 0 to 9 that occurs most frequently in n. Ties go to the larger digit.

Build It · mostFrequentDigit

Make every assert pass

Replace the placeholder return 42 with real logic, then run main(). Silence means every assert in testMostFrequentDigit passed; an AssertionError tells you exactly which case still fails.

Guided Exercise · Your Turn

Write hasConsecutiveDigits(n)

Write the function hasConsecutiveDigits(n) that takes a possibly-negative integer n and returns True if that number contains two consecutive digits that are the same, and False otherwise.

For example, 1223 has two consecutive 2's, but 1232 does not. A loop that compares each digit to the one right before it will catch this.

Build It · hasConsecutiveDigits

Make every assert pass

Replace the placeholder return 42 with real logic, then run main(). Silence means every assert in testHasConsecutiveDigits passed; an AssertionError tells you exactly which case still fails.